package com.tas.movies
{
	import com.abevi.utils.chunker.Chunker;
	import com.abevi.utils.chunker.ChunkerProgressEvent;
	import com.tas.editor.managers.MovieManager;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.filesystem.File;
	import flash.filesystem.FileMode;
	import flash.filesystem.FileStream;
	import flash.utils.ByteArray;
	
	import mx.collections.ArrayCollection;
	
	public class Movie extends EventDispatcher
	{		
		/**
		 * Constructor		 
		 */		
		public function Movie()
		{
			super();
			
			_properties = new ArrayCollection();	
			frames      = new ArrayCollection();
			
			addEventListener( MovieEvent.MOVIE_FILE_LOADED, loadProperties );		
			addEventListener( MovieEvent.PROPERTIES_LOADED, loadFrames );								
		}
		
		public static const START_SAVE  : String = "kStartsFromSaveState";
		public static const START_RESET : String = "kStartsFromReset";		
		public static const RATE_NTSC   : String = "kFrameRateNTSC";
		public static const RATE_PAL    : String = "kFrameRatePAL";
		
		private var _filename       : String;				
		private var _properties     : ArrayCollection;
		private var _numControllers : int = -1;
		private var _rawMovie       : ByteArray
		
		/**
		 * An internal reference to the raw bytes of the loaded file
		 *  
		 * @return 		 
		 */				
		protected function get rawMovie() : ByteArray
		{
			return _rawMovie;
		}
		
		/**
		 * <code>frames</code> is a collection of the visual representation of
		 * each frame of the movie. It is populated initially during the 
		 * <code>loadFrames()</code> routine. 
		 */		
		[Bindable]
		public var frames : ArrayCollection;
		
		/**
		 * Flag collection to indicate which controller(s) were used in the current
		 * movie file.
		 * 
		 * <p>The default collection only contains five (5) entries, as this is the 
		 * most available (through SNES), though I think Kega support up to 8, but
		 * that may be overkill for the current implementation.</p> 
		 */				
		protected var activeControllers : Array = [ false, false, false, false, false ];
		
		/**
		 * The <code>controllerMap</code> is a string lookup table. For movies
		 * of type <code>DATA_BYTES</code>, the sequence should correspond to the bits in 
		 * a frame (from right to left) of the input bytes.
		 * 
		 * <p>For movies of type <code>DATA_TEXT</code>, the sequence should match
		 * a controller record (from left to right) of the string value that represents
		 * a controller.</p>  
		 */		
		protected var controllerMap : Array;
		
		/**
		 * The chunker is provided as a convenience for subclasses to
		 * use to facilitate parsing frame collection initial loads 
		 */		
		protected var chunker : Chunker;
		
		protected static const DATA_BYTES : String = "kDataTypeBytes";
		protected static const DATA_TEXT  : String = "kDataTypeText";
		private var _dataType : String;
		private var _frameLength : int;
		
		/**
		 * Initialize the file contents data type. This is used to indicate how the
		 * contents need to be processed by <code>readFrame()</code> and 
		 * <code>writeFrame()</code>.
		 * 
		 * @param type should be either <code>DATA_BYTES</code> or <code>DATA_TEXT</code>
		 * @param frameLength the (optional) length of a frame (controller), in bytes
		 *                    (only used for <code>DATA_BYTES</code> movie types).
		 * 
		 */		
		protected function setDataType( type:String, frameLength:int=0 ) : void
		{
			_dataType    = type;
			_frameLength = frameLength;
		}		
		
		/**
		 * Initialize the <code>_numControllers</code> variable. This must be
		 * done by the subclass before the <code>numControllers</code> getter can
		 * be used.
		 *  
		 * @param value the controller count		  
		 */		
		protected function setControllerCount( value:int ) : void		
		{
			_numControllers = value;
		}			
		
		/**
		 * Get the number of controllers used in the current movie
		 *  
		 * @return 		 
		 */		
		public function get numControllers() : int
		{
			if( _numControllers < 0 ){
				throw new Error( "Controller Count must be initialized by setControllerCount() in the child class" );
			}else{
				return _numControllers;
			}
		}
		
		/**
		 * Used to track the position in the rawMovie
		 * when enumerating properties on load 
		 */		
		private var _fileSeekPointer : int = 0;								
		
		/**
		 * @protected
		 * 
		 * Get the pointer position (read-only) within the current file.
		 * This is a convenience when subclassing as the position is
		 * occasionally required for property registration length		  
		 * calculations
		 * 
		 * @return the file seek pointer 
		 */		
		protected function get fileSeekPointer() : int
		{
			return _fileSeekPointer;
		}
		
		public function get filename() : String
		{
			return _filename;
		}
		
		public function set filename( value:String ) : void
		{
			_filename = value;
			
			load();
		}
		
		/**
		 * Gets the frame count property registered to the current movie.
		 * If <code>frame_count</code> has not been registerd, an
		 * error is thrown.
		 *  
		 * @return total frames in the current movie file 		 
		 */		
		public function get frameCount() : uint
		{
			return getProperty( "frame_count" ).getValue();			
		}
		
		/**
		 * Gets the number of rerecords registered to the current movie.
		 * If <code>rerecord_count</code> has not been registerd, an
		 * error is thrown.
		 *  
		 * @return total rerecords in the current movie file 		 
		 */
		public function get rerecordCount() : uint
		{
			return getProperty( "rerecord_count" ).getValue();
		}
		
		/**
		 * @private
		 * 
		 * Fire a movie event
		 *  
		 * @param event		 
		 */		
		private function fireMovieEvent( event:String ) : void
		{
			dispatchEvent( new MovieEvent(event) );
		}
		
		/**
		 * @private
		 * 
		 * Load the physical file from the path in <code>filename</code> 		 
		 */		
		private function load() : void
		{
			var fs   : FileStream = new FileStream();
			var file : File = new File();
			file.nativePath = _filename;
			fs.open( file, FileMode.READ );
			
			fs.position = 0;
			
			_rawMovie = new ByteArray();
			fs.readBytes( _rawMovie, 0, fs.bytesAvailable );
			
			fs.close();
			fs = null;	
			
			fireMovieEvent( MovieEvent.MOVIE_FILE_LOADED );			
		}
		
		protected function loadProperties( e:Event=null ) : void
		{
			fireMovieEvent( MovieEvent.PROPERTIES_LOADED );
		}
		
		protected function loadFrames( e:Event=null ) : void
		{			
			fireMovieEvent( MovieEvent.MOVIE_LOADING );
			
			chunker = new Chunker();	
			chunker.addEventListener( ChunkerProgressEvent.COMPLETE, movieLoadComplete );
			chunker.addEventListener( ChunkerProgressEvent.PROGRESS, movieLoadProgress );											
		}
		
		/**
		 * @private
		 * 
		 * Listen for a <code>ChunkerProgressEvent</code> of type <code>PROGRESS</code>,
		 * and if caught, re-dispatch it
		 *  
		 * @param event		 
		 */		
		private function movieLoadProgress( event:ChunkerProgressEvent ) : void
		{
			dispatchEvent( event.clone() );			
			
			// TODO
			// This shouldn't be part of the Movie class. The manager should actually
			// intercept movie events and bubble or process them accordingly to ensure
			// everything is properly decoupled. For now though, this works :P
			MovieManager.getInstance().statusMessage = "Loading Frame " + event.position + " of " + event.length;
		}
		
		private function movieLoadComplete( event:ChunkerProgressEvent ) : void
		{
			chunker.removeEventListener( ChunkerProgressEvent.COMPLETE, movieLoadComplete );
			chunker.removeEventListener( ChunkerProgressEvent.PROGRESS, movieLoadProgress );
			chunker = null;
			
			fireMovieEvent( MovieEvent.MOVIE_LOADED );
			
			// TODO
			// This shouldn't be part of the Movie class. The manager should actually
			// intercept movie events and bubble or process them accordingly to ensure
			// everything is properly decoupled. For now though, this works :P
			MovieManager.getInstance().statusMessage = "Loaded " + _filename;
		}
		
		/**
		 * Register a property to the properties stack (collection). Properties are entered sequentially,
		 * so only the length of the current property needs to be known (position tracking is handled 
		 * by <code>fileSeekPointer</code>).
		 *  
		 * @param name
		 * @param length
		 * @param type
		 * 
		 * @return an instantiated @see com.tas.movies.MovieProperty object.The object is returned as a 
		 * 		   convenience in case a value needs to be extracted immediately, thereby removing
		 * 		   the extra step of calling <code>getProperty()</code> after registration.		 
		 */		
		protected function registerProperty( name:String, length:int, type:String=MovieProperty.TYPE_LITTLE_ENDIAN ) : MovieProperty
		{
			var value : ByteArray = new ByteArray();
			var start : int = _fileSeekPointer;
			_fileSeekPointer += length;
					
			for( var i:int = start; i < _fileSeekPointer; i++ ){
				value.writeByte( _rawMovie[i] );
			}
			
			var property : MovieProperty = new MovieProperty( name, type, value );
			_properties.addItem( property );			
			
			return property;
		}
		
		/**
		 * Lookup a property by name in the property stack. An error is thrown
		 * if the lookup fails
		 *  
		 * @param name the name of the property to look up
		 * 
		 * @return the property object from the stack		 
		 */		
		public function getProperty( name:String ) : MovieProperty
		{
			for each( var property:MovieProperty in _properties ){
				if( property.name == name ){
					return property;
				}
			}
			
			throw new Error( "Property '" + name + "' not found in collection" );
		}
		
		protected function readFrame( data:uint ) : String
		{
			var o : String = "";
			
			if( _dataType == DATA_BYTES ){
				if( controllerMap == null ){
					throw new Error( "controllerMap must be defined by the child class" );
				}
				for( var i:int = 0; i < controllerMap.length; i++ ){
					if( (1 & (data >> i )) == 1 ){
						o += controllerMap[i];
					}
				}	
			}
			
			return o;
		}
		
		protected function writeFrame( data:String ) : *
		{
			throw new Error( "writeFrame() must be overridden" );
		}

	}
}